/*
    Copyright(C) 2010 Ying-Chun Liu(PaulLiu). All rights reserved.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

package org.debian.paulliu.net;

import java.io.*;
import java.net.*;
import java.util.logging.*;

public class UDPHolePuncher
{
    private java.net.DatagramSocket socket;
    private java.net.InetSocketAddress remotePublicAddress;
    private java.net.InetSocketAddress remotePrivateAddress;
    private java.util.Random random = new java.util.Random();
    private int receivedPackets=0;
    private int sentPackets=0;

    public UDPHolePuncher(java.net.InetSocketAddress remotePublicAddress, java.net.InetSocketAddress remotePrivateAddress, java.net.DatagramSocket socket) {
	this.socket = socket;
	this.remotePublicAddress = remotePublicAddress;
	this.remotePrivateAddress = remotePrivateAddress;
    }

    public UDPHolePuncher(java.net.InetSocketAddress remotePublicAddress, java.net.DatagramSocket socket) {
	this.socket = socket;
	this.remotePublicAddress = remotePublicAddress;
	this.remotePrivateAddress = null;
    }

    public java.net.InetSocketAddress punch() {
	int oldTimeout=0;
	java.net.InetSocketAddress ret=null;
	boolean retIsPrivate = true;

	try {
	    oldTimeout=socket.getSoTimeout();
	    socket.setSoTimeout(1000);
	} catch (Exception e) {
	    Logger.getLogger("org.debian.paulliu").log(Level.SEVERE, "Cannot change Socket Timeout");
	}
	while (receivedPackets < 3 || sentPackets < 10) {
	    DatagramPacket receivedPacket = new DatagramPacket (new byte[2000], 2000);
	    sendUDPPacket();
	    sentPackets++;
	    try {
		socket.receive(receivedPacket);
		if (receivedPacket.getSocketAddress().equals(remotePublicAddress)) {
		    receivedPackets++;
		    retIsPrivate=false;
		} else if (remotePrivateAddress != null && receivedPacket.getSocketAddress().equals(remotePrivateAddress)) {
		    receivedPackets++;
		} else {
		    Logger.getLogger("org.debian.paulliu").log(Level.SEVERE, "Not allowed ip." + receivedPacket.getSocketAddress().toString() + "!=" + remotePrivateAddress.toString() + "Someone is hacking.");
		}
	    } catch (SocketTimeoutException e) {
	    } catch (Exception e) {
		Logger.getLogger("org.debian.paulliu").log(Level.SEVERE, "recv packet error");
	    }
	}
	for (int times=10; times>=0; times--) {
	    DatagramPacket receivedPacket = new DatagramPacket (new byte[2000], 2000);
	    try {
		socket.receive(receivedPacket);
		if (receivedPacket.getSocketAddress().equals(remotePublicAddress)) {
		    if (random.nextInt(2)==0) {
			sendUDPPacket();
			sentPackets++;
		    }
		    times = 10;
		    retIsPrivate=false;
		    receivedPackets++;
		} else if (remotePrivateAddress != null && receivedPacket.getSocketAddress().equals(remotePrivateAddress)) {
		    if (random.nextInt(2)==0) {
			sendUDPPacket();
			sentPackets++;
		    }
		    times = 10;
		    receivedPackets++;
		} else {
		    Logger.getLogger("org.debian.paulliu").log(Level.SEVERE, "Not allowed ip. Someone is hacking");
		}
	    } catch (SocketTimeoutException e) {
	    } catch (Exception e) {
		Logger.getLogger("org.debian.paulliu").log(Level.SEVERE, "recv packet error");
	    }
	}
	try {
	    socket.setSoTimeout(oldTimeout);
	} catch (Exception e) {
	    Logger.getLogger("org.debian.paulliu").log(Level.SEVERE, "Cannot set Socket Timeout back");
	}
	if (retIsPrivate && this.remotePrivateAddress != null) {
	    ret = this.remotePrivateAddress;
	} else if (!retIsPrivate && this.remotePublicAddress != null) {
	    ret = this.remotePublicAddress;
	}

	return ret;
    }

    private void sendUDPPacket() {
	if (this.remotePublicAddress != null) {
	    DatagramPacket emptyRemotePacket = null;
	    try {
		emptyRemotePacket = new DatagramPacket(new byte[0], 0, this.remotePublicAddress);
	    } catch (SocketException e) {
		Logger.getLogger("org.debian.paulliu").log(Level.SEVERE, "Cannot create emptyRemotePacket for public address");
	    }
	    try {
		socket.send(emptyRemotePacket);
	    } catch (IOException e) {
		Logger.getLogger("org.debian.paulliu").log(Level.SEVERE, "Cannot send emptyRemotePacket for public address");
	    }
	}
	if (this.remotePrivateAddress != null) {
	    DatagramPacket emptyRemotePacket = null;
	    try {
		emptyRemotePacket = new DatagramPacket(new byte[0], 0, this.remotePrivateAddress);
	    } catch (SocketException e) {
		Logger.getLogger("org.debian.paulliu").log(Level.SEVERE, "Cannot create emptyRemotePacket for private address: "+this.remotePrivateAddress.getAddress().getHostAddress());
	    }
	    try {
		socket.send(emptyRemotePacket);
	    } catch (IOException e) {
		Logger.getLogger("org.debian.paulliu").log(Level.SEVERE, "Cannot send emptyRemotePacket for private address: "+this.remotePrivateAddress.getAddress().getHostAddress());
	    }
	}
    }

    public int getReceivedPackets() {
	return receivedPackets;
    }

    public int getSentPackets() {
	return sentPackets;
    }
}
